from django.template import Context
from django.template.loader import get_template

class ProgrammerError(Exception):
    """Don't catch this. Fix the code instead."""
    pass

def parse_querystring(querystring):
    from django.http import QueryDict
    result = {}
    for k,v in QueryDict(querystring).lists():
        if len(v) == 1:
            result[k] = v[0]
        elif len(v) > 1:
            result[k] = v
    return result

def canonical_querystring(params):
    from urllib import urlencode, quote
    result = []
    for k in sorted(params.keys()):
        if hasattr(params,"getlist"):
            for v in sorted(params.getlist(k)):
                result.append(urlencode({k:v}))
        else:
            v = params[k]
            if isinstance(v,(tuple,list)):
                for v2 in sorted(v):
                    result.append(urlencode({k:v2}))
            else:
                result.append(urlencode({k:v}))
    return '&'.join(result)

def canonicalize_querystring(querystring):
    return canonical_querystring(parse_querystring(querystring))

class NamedQuerySetAlreadyDefined(Exception):
    pass

class NamedQuerySetNotDefined(Exception):
    pass

class NamedQuerySetIllFormed(Exception):
    """The named queryset was not defined properly."""
    pass

class NamedQuerySet(object):
    named_querysets = {}
    
    def __init__(self, slug, name, queryset, titler=None,
                 filters=[], defaults={}):
        if slug in NamedQuerySet.named_querysets:
            raise NamedQuerySetAlreadyDefined
        else:
            self._slug = slug
            self._name = name
            self._queryset = queryset
            if not titler:
                titler = lambda params:\
                 params['namedqueryset']['filtered']['querystring']
            self._titler = titler
            self._filters = tuple(filters)
            self._defaults = defaults
            self._filters_by_slug = {}
            self._expected_params = {}

            for f in filters:
                if f.slug in self._filters_by_slug:
                    raise NamedQuerySetIllFormed, \
                        "Filter slug %r was defined more than once." % f.slug
                self._filters_by_slug[f.slug] = f
            
            for f in self._filters:
                for name,external_name in f.params().iteritems():
                    if external_name in self._expected_params:
                        raise NamedQuerySetIllFormed, \
                              "%r defined multiple times!" % external_name
                    else:
                        self._expected_params[external_name] = f

            NamedQuerySet.named_querysets[slug] = self

    def filters_by_slug(self):
        return dict(self._filters_by_slug)
    
    def filters(self):
        return self._filters
    
    def get_content_type(cls, slug):
        from django.contrib.contenttypes.models import ContentType
        if slug not in cls.named_querysets:
            raise NamedQuerySetNotDefined, slug
        else:
            self = cls.named_querysets[slug]
            result = ContentType.objects.get_for_model(self._queryset.model)
            return result
    get_content_type = classmethod(get_content_type)
    
    def expected_params(cls, slug):
        """
        Returns an unordered sequence of all the external names given to the
        this NamedQuerySet's filters' parameters.
        """
        if slug not in cls.named_querysets:
            raise NamedQuerySetNotDefined, slug
        else:
            self = cls.named_querysets[slug]
            return self._expected_params.keys()
    expected_params = classmethod(expected_params)
    
    def get_context(cls, slug, params):
        if slug not in cls.named_querysets:
            raise NamedQuerySetNotDefined
        else:
            self = cls.named_querysets[slug]
            params = self._change_params(params)
            return params
    get_context = classmethod(get_context)
    
    def get_title(cls, slug, params):
        params = cls.get_context(slug, params)
        return params['namedqueryset']['filtered']['title']
    get_title = classmethod(get_title)
    
    def get_filtered_queryset(cls, slug, params):
        params = cls.get_context(slug, params)
        return params['namedqueryset']['filtered']['queryset']
    get_filtered_queryset = classmethod(get_filtered_queryset)
    
    def _change_params(self, params):
        queryset = self._queryset._clone()
        my_params = dict(self._defaults)
        for k in self._expected_params:
            try:
                my_params[k] = params[k]
            except KeyError:
                # try/except instead of "in" operator,
                # because Context's "in" doesn't do as expected.
                pass
        params.update(my_params)
        bookmark_querystring = canonical_querystring(my_params)
        for f in self._filters:
            queryset = f.filter(queryset,params)
        namedqueryset = {'slug': self._slug,
                         'name': self._name,
                         'queryset': self._queryset._clone(),
                         'filters': self._filters,
                         'filters_by_slug': self.filters_by_slug(),
                         'filtered':{'queryset':queryset,
                                     'querystring':bookmark_querystring}}
        params.update({'namedqueryset': namedqueryset})
        if self._titler:
            title= self._titler(params)
        else:
            title = ''
        params['namedqueryset']['filtered']['title'] = title
        return params
        
    def render(cls, slug, params, template_name):
        """
        Step 1: Takes the specified namedqueryset (slug specifies which one).
        Step 2: Passes the queryset through its filters (which read params)
        Step 3: Adds 'namedqueryset' to params.
        Step 4: Renders (and returns) the template, using params for context.
        
        Note that in addition to whatever is provided in the 'params' argument,
        the template can also read the following parameters:
            namedqueryset.slug - a string
            namedqueryset.name - a string
            namedqueryset.queryset - a Django QuerySet
            namedqueryset.filters - a sequence of AbstractQuerysetFilter objs
            namedqueryset.filters_by_slug - a dict from slug to filter
            namedqueryset.filtered.title - the filter-state, human-readable string
            namedqueryset.filtered.queryset - The QuerySet, filtered.
            namedqueryset.filtered.querystring -current filter state, URLencoded
        """
        if slug not in cls.named_querysets:
            raise NamedQuerySetNotDefined, slug
        else:
            self = cls.named_querysets[slug]
            params = self._change_params(params)
            ctx = Context(params)
            template = get_template(template_name)
            result = template.render(ctx)
            return result
    render = classmethod(render)
